package com.qen.truelicense.xml;

import org.apache.commons.codec.binary.Base64;

import java.beans.PersistenceDelegate;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.beans.PropertyVetoException;
import java.beans.VetoableChangeListener;
import java.beans.VetoableChangeSupport;
import java.beans.XMLEncoder;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.SignedObject;

/**
 * This non-visual JavaBean is a factory for authenticated runtime objects
 * whose integrity cannot be compromised without being detected.
 * The idea and the design of this class is inspired by both
 * {@link SignedObject} and
 * {@link java.security.cert.Certificate}.
 * <p>
 * More specifically, a {@code GenericCertificate} contains an XML string
 * encoded representation of an arbitrary object in the "encoded"
 * property and a Base64 immutable string representation of the object's
 * corresponding digital signature in the "signature" property.
 * The selection of this representation form and the design of this class
 * as a plain JavaBean allows its instances to be serialized using either
 * this package's {@link PersistenceService}, JDK's
 * {@link XMLEncoder}, or the vanilla {@link ObjectOutputStream}.
 * <p>
 * For an object to be successfully digitally signed, it must support
 * serialization via JDK's XMLEncoder, for which this package
 * provides the class {@code PersistenceService}.
 * This easy-to-use class allows you to provide custom
 * {@link PersistenceDelegate} instances for the serialisation of any
 * classes which do not implement the JavaBean design pattern and are not
 * supported by XMLEncoder as a default.
 * <p>
 * Whenever an instance of this GenericCertificate class is created,
 * you can arbitrarily set and get its "encoded" and "signature" properties,
 * allowing you to provide even custom deserialisation methods other than this
 * class already provides via the aforementioned classes. However, once this
 * instance is used to either sign or verify another object it gets locked,
 * allowing subsequent read access to its properties only.
 * <p>
 * The underlying signing algorithm is designated by the Signature
 * object passed to the {@code sign} and the {@code verify} methods.
 * <p>
 * A typical usage for signing is the following:
 * {@code <pre>
 * GenericCertificate cert = new GenericCertificate();
 * Signature signingEngine = Signature.getInstance(algorithm,
 *                                                 provider);
 * try {
 *     cert.sign(myObject, signingKey, signingEngine);
 * } catch (PropertyVetoException signingVetoed) {
 *     // ...
 * } catch (PersistenceServiceException serialisationFailed) {
 *     // ...
 * } catch (InvalidKeyException invalidKey) {
 *     // ...
 * } catch (SignatureException signingEngineBroken) {
 *     // ...
 * }
 * </pre>}
 * A typical usage for verification is the following (having
 * received GenericCertificate {@code cert}):
 * {@code <pre>
 * Signature verificationEngine =
 *     Signature.getInstance(algorithm, provider);
 * try {
 *     cert.verify(publicKey, verificationEngine));
 * } catch (PropertyVetoException verificationVetoed) {
 *     // ...
 * } catch (InvalidKeyException invalidKey) {
 *     // ...
 * } catch (SignatureException verificationEngineBroken) {
 *     // ...
 * } catch (GenericCertificateException integrityCompromised) {
 *     // ...
 * }
 * Object myObject = cert.getContent();
 * </pre>}
 * Several points are worth noting:
 * <ul><li>
 * There is no need to initialize the signing or verification engine,
 * as it will be re-initialized inside the {@link #sign} and {@link #verify}
 * methods. Secondly, for verification to succeed, the specified
 * public key must be the public key corresponding to the private key
 * used to sign the GenericCertificate.</li>
 * <li>
 * In contrast to SignedObject, this class adds more security
 * as it is impossible to retrieve the signed object without verifying
 * the signature before. A SignedObject however could
 * be deserialised from a compromised file and the application developer
 * may erraticaly forget to call the {@link SignedObject#verify}
 * method before retrieving the signed object by calling
 * {@link SignedObject#getObject()}.</li>
 * <li>
 * More importantly, for flexibility reasons, the
 * sign() and verify() methods allow for
 * customized signature engines, which can implement signature
 * algorithms that are not installed formally as part of a crypto
 * provider. However, it is crucial that the programmer writing the
 * verifier code be aware what {@link Signature} engine is being
 * used, as its own implementation of the {@link Signature#verify} method
 * is invoked to verify a signature. In other words, a malicious
 * Signature engine may choose to always return {@code true} on
 * verification in an attempt to bypass a security check.</li>
 * <li>
 * The signature algorithm could be, among others, the NIST standard
 * DSA, using DSA and SHA-1.  The algorithm is specified using the
 * same convention as that for signatures. The DSA algorithm using the
 * SHA-1 message digest algorithm can be specified, for example, as
 * "SHA/DSA" or "SHA-1/DSA" (they are equivalent).  In the case of
 * RSA, there are multiple choices for the message digest algorithm,
 * so the signing algorithm could be specified as, for example,
 * "MD2/RSA", "MD5/RSA" or "SHA-1/RSA". The algorithm name must be
 * specified, as there is no default.</li>
 * <li>
 * The name of the Cryptography Package Provider is designated
 * by the Signature parameter to the sign() and
 * verify() methods. If the provider is not specified,
 * the default provider is used. Each installation can be configured
 * to use a particular provider as default.</li>
 * <li>The property change listeners are <em>not</em> persistet when
 * using {@link ObjectOutputStream} or {@link XMLEncoder}.</li>
 * <li>{@link Object#equals(Object)} and {@link Object#hashCode()} are
 * <em>not</em> overridden by this class because different JVMs will produce
 * different literal encodings of the same object and we cannot rely on a proper
 * {@code equals(...)} implementation in the class of a signed object.</li>
 * </ul>
 * Potential applications of {@code GenericCertificate} include:
 * <ul>
 * <li>It can be used internally to any Java runtime as an unforgeable
 *     authorization token -- one that can be passed around without the
 *     fear that the token can be maliciously modified without being
 *     detected.</li>
 * <li>It can be used to sign and serialize data/object for storage outside
 *     the Java runtime (e.g., storing critical access control data on
 *     disk).</li>
 * <li>Nested {@code GenericCertificates} can be used to construct a logical
 *     sequence of signatures, resembling a chain of authorization and
 *     delegation.</li>
 * </ul>
 * <p>
 * This class is thread-safe.
 *
 * @author licz
 * @see Signature
 * @see SignedObject
 * @see java.security.cert.Certificate
 */
public final class GenericCertificate implements Serializable, XMLConstants {

    private static final long serialVersionUID = 6247620498526484734L;
    private static final String BASE64_CHARSET = "US-ASCII"; // NOI18N
    private static final String SIGNATURE_ENCODING = "US-ASCII/Base64"; // NOI18N

    /**
     * Holds value of property locked - is not serializable!!!
     */
    private transient volatile boolean locked;

    /**
     * Holds value of property encoded.
     */
    private String encoded;

    /**
     * Holds value of property signature.
     */
    private String signature;

    /**
     * Holds value of property signatureAlgorithm.
     */
    private String signatureAlgorithm;

    /**
     * Holds value of property signatureEncoding.
     */
    private String signatureEncoding;

    /**
     * Utility field used by bound properties.
     */
    private transient PropertyChangeSupport propertyChangeSupport;

    /**
     * Utility field used by constrained properties.
     */
    private transient VetoableChangeSupport vetoableChangeSupport;

    /**
     * Constructs a new generic certificate.
     */
    public GenericCertificate() {
    }

    /**
     * Copy constructor for the given generic certificate.
     * Note that the new certificate is unlocked and does not have any
     * event listeners.
     *
     * @param cert the generic certificate to copy.
     */
    public GenericCertificate(final GenericCertificate cert) {
        try {
            setEncoded(cert.getEncoded());
            setSignature(cert.getSignature());
            setSignatureAlgorithm(cert.getSignatureAlgorithm());
            setSignatureEncoding(cert.getSignatureEncoding());
        } catch (PropertyVetoException ex) {
            throw new AssertionError(ex);
        }
    }

    /**
     * Encodes and signs the given {@code content} in this certificate and
     * locks it.
     * <p>
     * Please note the following:
     * <ul>
     * <li>This method will throw a {@code PropertyVetoException} if this
     *     certificate is already locked, i.e. if it has been signed or
     *     verified before.</li>
     * <li>Because this method locks this certificate, a subsequent call to
     *     {@link #sign(Object, PrivateKey, Signature)} or
     *     {@link #verify(PublicKey, Signature)} is redundant
     *     and will throw a {@code PropertyVetoException}.
     *     Use {@link #isLocked()} to detect whether a
     *     generic certificate has been successfuly signed or verified before
     *     or call {@link #getContent()} and expect an
     *     Exception to be thrown if it hasn't.</li>
     * <li>There is no way to unlock this certificate.
     *     Call the copy constructor of {@link GenericCertificate} if you
     *     need an unlocked copy of the certificate.</li>
     * </ul>
     *
     * @param content       The object to sign. This must either be a JavaBean or an
     *                      instance of any other class which is supported by
     *                      {@code {@link PersistenceService}}
     *                      - maybe {@code null}.
     * @param signingKey    The private key for signing
     *                      - may <em>not</em> be {@code null}.
     * @param signingEngine The signature signing engine
     *                      - may <em>not</em> be {@code null}.
     * @throws NullPointerException                If the preconditions for the parameters
     *                                             do not hold.
     * @throws GenericCertificateIsLockedException If this certificate is
     *                                             already locked by signing or verifying it before.
     *                                             Note that this is actually a subclass of
     *                                             {@link PropertyVetoException}.
     * @throws PropertyVetoException               If locking the certifificate (and thus
     *                                             signing the object) is vetoed by any listener.
     * @throws PersistenceServiceException         If the object cannot be serialised.
     * @throws InvalidKeyException                 If the verification key is invalid.
     */
    public synchronized void sign(final Object content, final PrivateKey signingKey, final Signature signingEngine)
            throws NullPointerException, GenericCertificateIsLockedException, PropertyVetoException, PersistenceServiceException, InvalidKeyException {
        // Check status.
        final PropertyChangeEvent evt = new PropertyChangeEvent(this, "locked", Boolean.valueOf(locked), Boolean.TRUE); // NOI18N
        if (locked) {
            throw new GenericCertificateIsLockedException(evt);
        }

        // Check parameters.
        if (null == signingKey || null == signingEngine)
            throw new NullPointerException();

        // Notify vetoable listeners and give them a chance to veto.
        fireVetoableChange(evt);

        try {
            // Encode the object to bytes and sign it.
            final byte[] beo = PersistenceService.store2ByteArray(content);
            final String encoded = new String(beo, XML_CHARSET);
            signingEngine.initSign(signingKey);
            signingEngine.update(beo);
            final byte[] b64es = Base64.encodeBase64(signingEngine.sign());
            final String signature = new String(b64es, BASE64_CHARSET);
            final String algorithm = signingEngine.getAlgorithm();

            // Store results.
            setEncoded(encoded);
            setSignature(signature);
            setSignatureAlgorithm(algorithm);
            setSignatureEncoding(SIGNATURE_ENCODING); // NOI18N
        } catch (UnsupportedEncodingException ex) {
            throw new AssertionError(ex);
        } catch (SignatureException ex) {
            throw new AssertionError(ex);
        }

        // Lock this certificate and notify property change listeners.
        this.locked = true;
        firePropertyChange(evt);
    }

    /**
     * Verifies the digital signature of the encoded content in this
     * certificate and locks it.
     * <p>
     * Please note the following:
     * <ul>
     * <li>This method will throw a {@code PropertyVetoException} if this
     *     certificate is already locked, i.e. if it has been signed or
     *     verified before.</li>
     * <li>Because this method locks this certificate, a subsequent call to
     *     {@link #sign(Object, PrivateKey, Signature)} or
     *     {@link #verify(PublicKey, Signature)} is redundant
     *     and will throw a {@code PropertyVetoException}.
     *     Use {@link #isLocked()} to detect whether a
     *     generic certificate has been successfuly signed or verified before
     *     or call {@link #getContent()} and expect an
     *     Exception to be thrown if it hasn't.</li>
     * <li>There is no way to unlock this certificate.
     *     Call the copy constructor of {@link GenericCertificate} if you
     *     need an unlocked copy of the certificate.</li>
     * </ul>
     *
     * @param verificationKey    The public key for verification
     *                           - may <em>not</em> be {@code null}.
     * @param verificationEngine The signature verification engine
     *                           - may <em>not</em> be {@code null}.
     * @throws NullPointerException                 If the preconditions for the parameters
     *                                              do not hold.
     * @throws GenericCertificateIsLockedException  If this certificate is
     *                                              already locked by signing or verifying it before.
     *                                              Note that this is actually a subclass of
     *                                              {@link PropertyVetoException}.
     * @throws PropertyVetoException                If locking the certifificate (and thus
     *                                              verifying the object) is vetoed by any listener.
     * @throws InvalidKeyException                  If the verification key is invalid.
     * @throws SignatureException                   If signature verification failed.
     * @throws GenericCertificateIntegrityException If the integrity of this
     *                                              certificate has been compromised.
     */
    public synchronized void verify(final PublicKey verificationKey, final Signature verificationEngine)
            throws NullPointerException, GenericCertificateIsLockedException, PropertyVetoException, InvalidKeyException, SignatureException, GenericCertificateIntegrityException {
        // Check status.
        final PropertyChangeEvent evt = new PropertyChangeEvent(this, "locked", Boolean.valueOf(locked), Boolean.TRUE); // NOI18N
        if (locked) {
            throw new GenericCertificateIsLockedException(evt);
        }

        // Check parameters.
        if (null == verificationKey || null == verificationEngine) {
            throw new NullPointerException();
        }

        // Notify vetoable listeners and give them a chance to veto.
        fireVetoableChange(evt);

        try {
            // Get the byte encoded object and verify it.
            final byte[] beo = encoded.getBytes(XML_CHARSET);
            verificationEngine.initVerify(verificationKey);
            verificationEngine.update(beo);
            final byte[] b64ds = Base64.decodeBase64(
                    signature.getBytes(BASE64_CHARSET));
            if (!verificationEngine.verify(b64ds))
                throw new GenericCertificateIntegrityException();
            final String algorithm = verificationEngine.getAlgorithm();

            // Reset signature parameters.
            setSignatureAlgorithm(algorithm);
            setSignatureEncoding(SIGNATURE_ENCODING);
        } catch (UnsupportedEncodingException ex) {
            throw new AssertionError(ex);
        }

        // Lock this certificate and notify property change listeners.
        this.locked = true;
        firePropertyChange(evt);
    }

    /**
     * Returns the value of the property {@code locked}.
     * If {@code true}, an object was successfully signed or verified
     * before and a clone can be safely retrieved using
     * {@code getContent()}.
     *
     * @return The value of the property {@code locked}.
     */
    public boolean isLocked() {
        return this.locked;
    }

    /**
     * Returns a clone of the certificate's content as it was signed or
     * verified before.
     * You should save the returned object for later use as each call
     * to this method is pretty expensive in terms of runtime and
     * memory. This method may return {@code null} if this has been
     * signed before.
     *
     * @return A clone of the certificate's content as it was signed or
     * verified before.
     * @throws GenericCertificateNotLockedException If no content has been
     *                                              signed or verified before.
     *                                              Note that this is ultimately a {@link RuntimeException}.
     * @throws PersistenceServiceException          If the signed object cannot get
     *                                              reinstantiated from its XML representation for some reason.
     *                                              This may happen for example if the signed object was created
     *                                              by a more recent version of its class which contains additional
     *                                              properties which are not supported by earlier versions.
     */
    public synchronized Object getContent() throws GenericCertificateNotLockedException, PersistenceServiceException {
        if (!locked) {
            throw new GenericCertificateNotLockedException();
        }
        return PersistenceService.load(encoded);
    }

    /**
     * The value of the property {@code encoded}.
     * The default is {@code null}.
     *
     * @return The value of the property {@code encoded}.
     */
    public synchronized String getEncoded() {
        return this.encoded;
    }

    /**
     * Setter for the bound property {@code encoded}.
     *
     * @param encoded The new encoded representation of the signed object
     *                - may be {@code null}.
     * @throws GenericCertificateIsLockedException If this certificate is
     *                                             already locked by signing or verifying it before.
     *                                             Note that this is actually a subclass of
     *                                             {@link PropertyVetoException}.
     */
    public synchronized void setEncoded(final String encoded) throws GenericCertificateIsLockedException {
        // Check status.
        final PropertyChangeEvent evt = new PropertyChangeEvent(this, "encoded", this.encoded, encoded); // NOI18N
        if (locked) {
            throw new GenericCertificateIsLockedException(evt);
        }

        // Check parameters.
        if (equals(this.encoded, encoded)) {
            return;
        }

        //vetoableChangeSupport.fireVetoableChange(evt); // Incompatible to sign!
        this.encoded = encoded;
        firePropertyChange(evt);
    }

    /**
     * Getter for the property {@code signature}.
     * The default is {@code null}.
     *
     * @return Value of property signature.
     */
    public synchronized String getSignature() {
        return this.signature;
    }

    /**
     * Setter for the bound property {@code signature}.
     *
     * @param signature The signature encoded as a string
     *                  - may be {@code null}.
     * @throws GenericCertificateIsLockedException If this certificate is
     *                                             already locked by signing or verifying it before.
     *                                             Note that this is actually a subclass of
     *                                             {@link PropertyVetoException}.
     */
    public synchronized void setSignature(final String signature) throws GenericCertificateIsLockedException {
        // Check status.
        final PropertyChangeEvent evt = new PropertyChangeEvent(this, "signature", this.signature, signature); // NOI18N
        if (locked) {
            throw new GenericCertificateIsLockedException(evt);
        }

        // Check parameters.
        if (equals(this.signature, signature)) {
            return;
        }

        //vetoableChangeSupport.fireVetoableChange(evt); // Incompatible to sign!
        this.signature = signature;
        firePropertyChange(evt);
    }

    /**
     * Getter for the property {@code signatureAlgorithm}.
     * The default is {@code null}.
     *
     * @return The signature algorithm.
     */
    public synchronized String getSignatureAlgorithm() {
        return this.signatureAlgorithm;
    }

    /**
     * Setter for the bound property {@code signatureAlgorithm}.
     *
     * @param signatureAlgorithm The string identifying the signature algorithm
     *                           - may be {@code null}.
     * @throws GenericCertificateIsLockedException If this certificate is
     *                                             already locked by signing or verifying it before.
     *                                             Note that this is actually a subclass of
     *                                             {@link PropertyVetoException}.
     */
    public synchronized void setSignatureAlgorithm(final String signatureAlgorithm) throws GenericCertificateIsLockedException {
        // Check status.
        final PropertyChangeEvent evt = new PropertyChangeEvent(this, "signatureAlgorithm", this.signatureAlgorithm, signatureAlgorithm); // NOI18N
        if (locked) {
            throw new GenericCertificateIsLockedException(evt);
        }

        // Check parameters.
        if (equals(this.signatureAlgorithm, signatureAlgorithm)) {
            return;
        }

        //vetoableChangeSupport.fireVetoableChange(evt); // Incompatible to sign!
        this.signatureAlgorithm = signatureAlgorithm;
        firePropertyChange(evt);
    }

    /**
     * Getter for the property {@code signatureEncoding}.
     * The default is {@code null}.
     *
     * @return The character encoding of the signature string.
     */
    public synchronized String getSignatureEncoding() {
        return signatureEncoding;
    }

    /**
     * Setter for the bound property {@code signatureEncoding}.
     *
     * @param signatureEncoding The string identifying the signature
     *                          encoding - may be {@code null}.
     * @throws GenericCertificateIsLockedException If this certificate is
     *                                             already locked by signing or verifying it before.
     *                                             Note that this is actually a subclass of
     *                                             {@link PropertyVetoException}.
     * @deprecated Currently ignored by {@link #verify}.
     * Only provided to cause {@link XMLEncoder} to encode this
     * property for upwards compatibility.
     */
    public synchronized void setSignatureEncoding(final String signatureEncoding) throws GenericCertificateIsLockedException {
        // Check status.
        final PropertyChangeEvent evt = new PropertyChangeEvent(this, "signatureEncoding", this.signatureEncoding, signatureEncoding); // NOI18N
        if (locked) {
            throw new GenericCertificateIsLockedException(evt);
        }

        // Check parameters.
        if (equals(this.signatureEncoding, signatureEncoding)) {
            return;
        }

        //vetoableChangeSupport.fireVetoableChange(evt); // Incompatible to sign!
        this.signatureEncoding = signatureEncoding;
        firePropertyChange(evt);
    }

    private static boolean equals(Object a, Object b) {
        return a == b || null != a && a.equals(b);
    }

    //
    // Property handling methods.
    //

    /**
     * Adds a VetoableChangeListener to the listener list.
     *
     * @param l The listener to add.
     * @deprecated Not required.
     */
    public synchronized void addVetoableChangeListener(VetoableChangeListener l) {
        if (null == vetoableChangeSupport) {
            vetoableChangeSupport = new VetoableChangeSupport(this);
        }
        vetoableChangeSupport.addVetoableChangeListener(l);
    }

    /**
     * Removes a VetoableChangeListener from the listener list.
     *
     * @param l The listener to remove.
     * @deprecated Not required.
     */
    public synchronized void removeVetoableChangeListener(VetoableChangeListener l) {
        if (null == vetoableChangeSupport) {
            return;
        }
        vetoableChangeSupport.removeVetoableChangeListener(l);
    }

    /**
     * @deprecated Not required.
     */
    private void fireVetoableChange(PropertyChangeEvent evt) throws PropertyVetoException {
        if (null == vetoableChangeSupport) {
            return;
        }
        vetoableChangeSupport.fireVetoableChange(evt);
    }

    /**
     * Adds a PropertyChangeListener to the listener list.
     *
     * @param l The listener to add.
     * @deprecated Not required.
     */
    public synchronized void addPropertyChangeListener(PropertyChangeListener l) {
        if (null == propertyChangeSupport) {
            propertyChangeSupport = new PropertyChangeSupport(this);
        }
        propertyChangeSupport.addPropertyChangeListener(l);
    }

    /**
     * Removes a PropertyChangeListener from the listener list.
     *
     * @param l The listener to remove.
     * @deprecated Not required.
     */
    public synchronized void removePropertyChangeListener(PropertyChangeListener l) {
        if (null == propertyChangeSupport) {
            return;
        }
        propertyChangeSupport.removePropertyChangeListener(l);
    }

    /**
     * @deprecated Not required.
     */
    private void firePropertyChange(PropertyChangeEvent evt) {
        if (null == propertyChangeSupport) {
            return;
        }
        propertyChangeSupport.firePropertyChange(evt);
    }
}
